<?php

/**
 * Instances of <code>SpicaPatternUrlResolver</code> is capable of resolving
 * URLs into meaningful parts to Spica dispatcher, based on routing configuration.
 *
 * A route string is a string with 2 special constructions:
 *    * - :string: :string denotes a named paramater
 *    (available later as SpicaRequest::get('string'))
 *    * - *: * match an indefinite number of parameters in a route
 *
 * @category   spica
 * @package    PatternUrlResolver
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 14, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: SpicaPatternUrlResolver.php 1294 2009-07-14 15:17:25Z pcdinh $
 */
class SpicaPatternUrlResolver implements SpicaPathResolver
{
    /**
     * Mapping between route name to URL parts used in dynamic routes
     *
     * @var array
     */
    protected $_dynamic;

    /**
     * Mapping between route name to controllers components (controller, action,
     * module, package)
     *
     * @var array
     */
    protected $_actions;
    protected $_reqs;    // requirements for route arguments
    protected $_passive; // route => URL mappings (for passive routes)
    protected $_static;  // route => URL mappings (for static routes)
    protected $_urls;    // saved URLs for dynamic and static routes
    protected $_failed;  // fail routes with error codes and messages

    protected $base_url;      // helps us to construct full URL
    protected $url_rewriting; // shall we asume mod_rewrite functionality

    public $current; // Currently selected route (automatically set by dispatch)


    /**
     * Profile routing config.
     *
     * @var array
     */
    protected static $_config;

    /**
     * Constructs an object of <code>SpicaPatternUrlResolver</code>
     *
     * @param array $config
     */
    public function __construct($config)
    {
        self::$_config = $config;
    }

    /**
     *
     */
    public function addRoute($name, $url, $map, $requirements = array())
    {
        if (!empty($controller) || !empty($action))
        {
            $this->_urls[$name]    = $url;
            $this->_actions[$name] = $map;

            $isDynamic = false;
            $parts     = explode('/', $url);
            foreach ($parts as $offset => $part)
            {
                // $part can not be empty
                if (($part[0]  == ':' || $part[0] == '*'))
                {
                    $isDynamic = true;
                    $this->_args[$name] = $part;
                }
            }

            if ($isDynamic)
            {
                $this->_dynamic[$name] = $parts;

                if (!empty($requirements))
                {
                    $this->_reqs[$name] = $requirements;
                }
            }
            else
            {
                $this->_static[$name] = $parts;
            }
        }
        else
        {
            $this->_passive[$name] = $url;
        }
    }

    /**
     * Resolves URLs into parts and populates the global $_GET.
     *
     * @param string $url
     */
    public function resolve($url)
    {

    }

    /**
     * Extract route arguments for some matching url
     *
     * As usual, arguments named "parts" are arrays of path components.
     */
    protected function extractArgs(array $route_parts, array $url_parts)
    {
        $args = array();

        $i = 0;
        foreach ($route_parts as $offset => $part)
        {
            if ($part[0] == ':')
            {
                $args[substr($part, 1)] = $url_parts[$offset];
            }
            elseif ($part[0] == '*')
            {
                /* Wildcard arg can contain slashes, join all parts after '*'
                 * Additionally, it must be last argument, return imediately.
                 */
                $args[substr($part, 1)] = implode('/', array_slice($url_parts, $i));
                return $args;
            }
            $i++;
        }

        return $args;
    }

    /**
     * Check each route arguments against corresponding
     * requirement in RouteMap::_reqs.
     *
     * Requirement *must* be valid perl regular expression.
     * It only makes sense to do full string matching for requirements
     * so ^ and $ are *always* added automatically.
     */
    protected function check_reqs($route_name, array $route_args)
    {
        if (empty($this->_reqs[$route_name]))
        {
            return True;
        }

        foreach ($this->_reqs[$route_name] as $arg_name => $arg_req)
        {
            if (!preg_match('/^' . $arg_req . '$/', $route_args[$arg_name]))
            {
                return False;
            }
        }

        return True;
    }

    /**
     * Matches the given URI against the route.
     *
     * @param string $url
     */
    public function match($url)
    {
        // Match static route
        $routeName      = array_search($url, $this->_urls);
        if ($routeName !== false && isset($this->_static[$routeName]))
        {
            return $routeName;
        }

        unset($routeName);

        // Match dynamic route
        $urlParts        = explode('/', $url);
        $len             = count($urlParts);
        $dynamicRoutes   = $this->_dynamic;
        $matchedWildcard = null;

        /* Find potential candidate(s), always preferring routes with
         * static parts e.g. for routes
         *   ':category/:article' and 'news/:article'
         * and url
         *   'news/something'
         * we can safely discard the first route, otherwise we would have to
         * specify requirement for the first: 'category' => '(?!(news)).*'
         */
        foreach ($urlParts as $offset => $part)
        {
            $dynPartRoutes    = array();
            $staticPartRoutes = array();

            // Match configured dynamic part first
            foreach ($dynamicRoutes as $routeName => $predefinedParts)
            {
                // No predefined part at the offset
                if (count($predefinedParts) <= $offset)
                {
                    // route too short
                    continue;
                }

                // URL: news - route: :news
                if ($predefinedParts[$offset][0] != ':' && $predefinedParts[$offset] == $part)
                {
                    $staticPartRoutes[$routeName] = $predefinedParts;
                }
                elseif ($predefinedParts[$offset][0] == ':') // named pattern
                {
                    $dynPartRoutes[$routeName] = $predefinedParts;
                }
                elseif ($predefinedParts[$offset][0] == '*') // wildcard pattern
                {
                    // route and url contain * at same place is not allowed
                    $wild_args = $this->extractArgs($predefinedParts, $urlParts);

                    if ($this->check_reqs($routeName, $wild_args))
                    {
                        if (!$matchedWildcard)
                        {
                            $matchedWildcard = $this->match_failed(array($routeName,
                            $this->_actions[$routeName], $wild_args));
                        }

                        throw new ERouteMapNoReqs();
                    }
                    else
                    {
                        /* Some parts of wildcard route didn't match,
                         it will be discarded. */
                        continue;
                    }
                }
            }

            if (!empty($staticPartRoutes))
            {
                $dynamicRoutes = $staticPartRoutes;
            }
            else
            {
                $dynamicRoutes = $dynPartRoutes;
            }
        }

        if (count($dynamicRoutes) == 0)
        {
            if (!$matchedWildcard)
            {
                throw new ERouteMapNoMatch();
            }

            return $matchedWildcard;
        }
        elseif (count($dynamicRoutes) == 1)
        {
            /* Anyone knows how to extract key->value pair from a dictionary
             * with length=1 where we don't know the key?
             */
            foreach ($dynamicRoutes as $name => $route_parts)
            {
                $routeName = $name;
                $route_args = $this->extractArgs($route_parts, $urlParts);
            }

            if (count($this->_dynamic[$routeName]) != $len)
            {
                if (!$matchedWildcard)
                {
                    // false match, actual route is longer
                    throw new ERouteMapNoMatch();
                }

                return $matchedWildcard;
            }

            if ($this->check_reqs($routeName, $route_args))
            {
                return $this->match_failed(array($routeName, $this->_actions[$routeName], $route_args));
            }
            else
            {
                if (!$matchedWildcard)
                {
                    throw new ERouteMapReqs();
                }

                return $matchedWildcard;
            }
        }
        elseif (count($dynamicRoutes) > 1)
        {
            // filter out false matches
            $real_matched = array();

            foreach ($dynamicRoutes as $n_name => $n_parts)
            {
                if (count($n_parts) == $len)
                {
                    $real_matched[$n_name] = $n_parts;
                }
            }

            if (empty($real_matched))
            {
                if (!$matchedWildcard)
                {
                    throw new ERouteMapNoMatch();
                }
                else
                {
                    return $matchedWildcard;
                }
            }

            /* OK, we have N routes with same signature
             * At least N-1 routes *must* specify requirements. We try to match
             * dynamic route with requirements. If requirements are not
             * satisfied we select route without requirements (if such route
             * exists). Otherwise we fail.
             */

            $no_req_seen = False; // have we seen route without arguments?
            $no_req_name = NULL;
            $no_req_parts = NULL;

            foreach ($real_matched as $n_name => $n_parts)
            {
                if (empty($this->_reqs[$n_name]))
                {
                    if ($no_req_seen)
                    {
                        if (!$matchedWildcard)
                        {
                            throw new ERouteMapNoReqs();
                        }
                        else
                        {
                            return $matchedWildcard;
                        }
                    }
                    else
                    {
                        $no_req_seen = True;
                        $no_req_name = $n_name;
                        $no_req_parts = $n_parts;
                    }
                }
                else
                {
                    $n_args = $this->extractArgs($n_parts, $urlParts);

                    if ($this->check_reqs($n_name, $n_args))
                    {
                        // We have a winner, return immediately
                        return $this->match_failed(array($n_name,
                        $this->_actions[$n_name], $n_args));
                    }
                }
            }

            /**
             * OK, at this point we either have "safe" route, without
             * requirements, or none of the duplicates fulfills requirements.
             */
            if ($no_req_seen)
            {
                $no_req_args = $this->extractArgs($no_req_parts, $urlParts);

                return $this->match_failed(array($no_req_name,
                $this->_actions[$no_req_name], $no_req_args));
            }
            else
            {
                if (!$matchedWildcard)
                {
                    throw new ERouteMapReqs();
                }
                else
                {
                    return $matchedWildcard;
                }
            }
        }

    }
}

?>